
上一节介绍了在C++编程中遇到的大多数重载情况。但对于这些规则，还有更多的规则和例外——本书并不是转为C++函数重载所著，因此不会一一介绍。但我们在这里会讨论其中一些规则，一方面是因为它们比其他规则应用得更多，另一方面是为了让大家了解其中的水有多深。

\subsubsubsection{C.3.1\hspace{0.2cm}选择非模板或更特化的临时模板}

当重载解析的所有方面都相同时，非模板函数优先于模板的实例(该实例是从泛型模板定义生成，还是作为显式特化提供，都无关紧要)。例如:

\begin{lstlisting}[style=styleCXX]
template<typename T> int f(T); // #1
void f(int); // #2

int main()
{
	return f(7); // ERROR: selects #2 , which doesn’t return a value
}
\end{lstlisting}

这个例子还清楚地说明了重载解析通常不涉及所选函数的返回类型。

但当重载解析的其他方面略有不同时(具有不同的const和引用限定符)，首先应用重载解析的一般规则。当成员函数定义为接受与复制或移动构造函数相同的参数时，这种方式就很容易导致不可预期的行为。参见16.2.4节。

如果要在两个模板之间进行选择，首选最特化的模板(前提是其中一个实际上比另一个更特化)。关于这个概念的详细解释，请参阅16.2.2节。这种区别的特殊情况发生在两个模板仅在添加末尾参数包时:没有包的模板更特化，因此若与调用相匹配将首先选择。4.1.2节讨论了这种情况的一个例子。

\subsubsubsection{C.3.2\hspace{0.2cm}转换序列}

隐式转换通常可以是转换的序列。考虑下面的代码示例:

\begin{lstlisting}[style=styleCXX]
class Base {
	public:
	operator short() const;
};

class Derived : public Base {
};

void count(int);

void process(Derived const& object)
{
	count(object); // matches with user-defined conversion
}
\end{lstlisting}

调用count(object)有效，是因为object可以隐式转换为int。这种转换需要几步:

\begin{enumerate}
\item 
从派生const对象到基本const对象的转换(这是glvalue转换，保留了对象的身份)

\item 
将生成的Base const对象转换为short类型的用户定义转换

\item 
short升为int
\end{enumerate}

这是最常见的转换序列:标准转换(在本例中是派生到基类的转换)，然后是用户定义的转换，然后是另一个标准转换。虽然在转换序列中最多只能有一个用户定义的转换，但也可能只有标准转换。

重载解析的一个重要原则是，作为另一个转换序列的子序列的转换序列要优于后一个序列。若有其他候选函数

\begin{lstlisting}[style=styleCXX]
void count(short);
\end{lstlisting}

因为不需要转换序列中的第三步(类型提升)，所以调用count(object)会将其作为首选。

\subsubsubsection{C.3.3\hspace{0.2cm}指针转换}

指针和指向成员的指针进行各种特殊的标准转换，包括

\begin{itemize}
\item 
转换为bool类型

\item 
将指针类型转换为void*

\item 
派生类的指针转换为基类指针

\item 
成员指针转换为派生类指针
\end{itemize}

尽管所有这些都可以“只与标准转换匹配”，但排名并不相同。

首先，到bool类型的转换(无论是从普通指针还是从指向成员的指针)，认为比其他类型的标准转换都要糟糕。例如:

\begin{lstlisting}[style=styleCXX]
void check(void*); // #1
void check(bool); // #2

void rearrange (Matrix* m)
{
	check(m); // calls #1
	...
}
\end{lstlisting}

常规指针转换的类别中，void*类型的转换认为比从派生类指针到基类指针的转换更糟糕。此外，若存在到与继承相关的不同类的转换，则首选到最派生类的转换。下面是另一个例子:

\begin{lstlisting}[style=styleCXX]
class Interface {
	...
};

class CommonProcesses : public Interface {
	...
};

class Machine : public CommonProcesses {
	...
};

char* serialize(Interface*); // #1
char* serialize(CommonProcesses*); // #2

void dump (Machine* machine)
{
	char* buffer = serialize(machine); // calls #2
	...
}
\end{lstlisting}

从Machine*到CommonProcesses*的转换优于到Interface*的转换，后者非常直观。

类似的规则也适用于成员指针:相关的指针成员类型的两种转换之间，优先选择继承图中“最接近的基”(即派生最少的基)。

\subsubsubsection{C.3.4\hspace{0.2cm}初始化式列表}

初始化列表参数(初始化列表参数用大括号传递)可以转换为几种不同类型的参数:initializer\_lists、带有initializer\_list构造函数的类类型、初始化列表元素可以作为构造函数(单独)参数的类类型，或者聚合类类型，其成员可以由初始化列表的元素初始化。下面的程序阐明了这些情况:

\hspace*{\fill} \\ %插入空行
\noindent
\textit{overload/initlist.cpp}
\begin{lstlisting}[style=styleCXX]
#include <initializer_list>
#include <string>
#include <vector>
#include <complex>
#include <iostream>

void f(std::initializer_list<int>) {
	std::cout << "#1\n";
}

void f(std::initializer_list<std::string>) {
	std::cout << "#2\n";
}

void g(std::vector<int> const& vec) {
	std::cout << "#3\n";
}

void h(std::complex<double> const& cmplx) {
	std::cout << "#4\n";
}

struct Point {
	int x, y;
};
void i(Point const& pt) {
	std::cout << "#5\n";
}

int main()
{
	f({1, 2, 3}); // prints #1
	f({"hello", "initializer", "list"}); // prints #2
	g({1, 1, 2, 3, 5}); // prints #3
	h({1.5, 2.5}); // prints #4
	i({1, 2}); // prints #5
}	
\end{lstlisting}

在对f()的前两次调用中，初始化式列表参数转换为std::initializer\_list值，这将初始化式列表中的每个元素转换为std::initializer\_list的元素类型。第一个调用中，所有元素都已经是int类型，因此不需要进行额外的转换。第二次调用中，通过调用string(char const*)构造函数，初始化式列表中的每个字符串字面值都转换为std::string。第三个调用(g())使用std::vector(std::initializer\_list<int>)构造函数执行用户定义的转换。下一个调用std::complex(double, double)构造函数，就好像编写了std::complex<double>(1.5, 2.5)。最后一个调用执行聚合初始化，从初始化式列表中的元素初始化Point类实例的成员，而不调用Point的构造函数。

\begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black]
\hspace*{0.75cm}聚合初始化只适用于C++中的聚合类型，即数组或简单的类C，这些类没有用户提供的构造函数，没有private或protected的非静态数据成员，没有基类，也没有虚函数。C++14前，也不能有默认的成员初始化式。C++17后，可以使用公共基类。
\end{tcolorbox}

对于初始化式列表，有几种重载情况。当将初始化式列表转换为initializer\_list时，如上例的前两次调用，整个转换排名与初始化式列表中给定元素到initializer\_list元素类型的最差转换相同(即initializer\_list<T>中的T)。这可能会导致一些意外出现，比如下面的例子:

\hspace*{\fill} \\ %插入空行
\noindent
\textit{overload/initlist.cpp}
\begin{lstlisting}[style=styleCXX]
#include <initializer_list>
#include <iostream>

void ovl(std::initializer_list<char>) { // #1
	std::cout << "#1\n";
}

void ovl(std::initializer_list<int>) { // #2
	std::cout << "#2\n";
}

int main()
{
	ovl({’h’, ’e’, ’l’, ’l’, ’o’, ’\0’}); // prints #1
	ovl({’h’, ’e’, ’l’, ’l’, ’o’, 0}); // prints #2
}
\end{lstlisting}

对ovl()的第一次调用中，初始化式列表的每个元素都是一个字符。对于第一个ovl()函数，这些元素根本不需要转换。对于第二个ovl()函数，这些元素需要提升为int。因为完美匹配比类型升级要好，所以对ovl()的第一次调用的是\#1。

对ovl()的第二次调用中，前五个元素是char类型，而最后一个是int类型。对于第一个ovl()函数，char元素完美匹配，但是int需要标准转换，因此整个转换为标准转换。对于第二个ovl()函数，char元素需要升级为int，而末尾的int元素完全匹配。第二个ovl()函数的整体转换排名为提升，这使的它比第一个ovl()函数更为合适(尽管只有单个元素的转换更好)。

当使用初始化式列表初始化类对象时，就像在我们的原始示例中调用g()和h()一样，重载解析分为两个阶段进行:

\begin{enumerate}
\item
第一阶段只考虑初始化列表构造函数，对于某些类型T，其唯一非默认参数是std::initializer\_list<T>类型的构造函数(删除顶层引用和const/volatile限定符之后)。

\item
若没有找到这样的可行构造函数，第二阶段将考虑其他构造函数。
\end{enumerate}

该规则有一个例外:若初始化列表为空，并且类有一个默认构造函数，则跳过第一阶段，以便调用默认构造函数。

该规则的效果是，任何初始化式列表构造函数，比非初始化式列表构造函数更匹配:

\hspace*{\fill} \\ %插入空行
\noindent
\textit{overload/initlistctor.cpp}
\begin{lstlisting}[style=styleCXX]
#include <initializer_list>
#include <string>
#include <iostream>

template<typename T>
struct Array {
	Array(std::initializer_list<T>) {
		std::cout << "#1\n";
	}
	Array(unsigned n, T const&) {
		std::cout << "#2\n";
	}
};

void arr1(Array<int>) {
}

void arr2(Array<std::string>) {
}

int main()
{
	arr1({1, 2, 3, 4, 5}); // prints #1
	arr1({1, 2}); // prints #1
	arr1({10u, 5}); // prints #1
	arr2({"hello", "initializer", "list"}); // prints #1
	arr2({10, "hello"}); // prints #2
}
\end{lstlisting}

第二个构造函数接受一个unsigned和一个T const\&参数，从初始化列表初始化Array<int>对象时不会调用，因为其初始化列表构造函数总是比非初始化列表构造函数更匹配。但对于Array<string>，当初始化列表构造函数不可用时，就会调用非初始化列表构造函数，就像对arr2()的第二次调用一样。

\subsubsubsection{C.3.5\hspace{0.2cm}函子和代理函数}

查找函数名以创建初始重载集之后，可以各种方式调整该集合。当调用表达式引用类类型对象而不是函数时，会出现一种有趣的情况，重载集会增加。

第一个方式很简单:将成员的函数操作符(函数调用操作符)添加到集合中。带有此类操作符的对象通常称为函子或函数对象(参见第11.1节)。

当类对象包含指向函数类型指针(或指向函数类型引用)的隐式转换操作符时，重载集就会出现不那么明显的增加。

\begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black]
\hspace*{0.75cm}转换操作符也必须适用于以下情况，例如：const对象不考虑非const操作符。
\end{tcolorbox}

这种情况下，将向重载集添加一个虚拟(或代理)函数。除了具有与该转换函数的目标类型中，参数类型对应的参数外，这个代理函数候选函数认为具有转换函数指定的类型的参数。下面的例子清楚地说明了这一点:

\begin{lstlisting}[style=styleCXX]
using FuncType = void (double, int);

class IndirectFunctor {
	public:
	...
	void operator()(double, double) const;
	operator FuncType*() const;
};

void activate(IndirectFunctor const& funcObj)
{
	funcObj(3, 5); // ERROR: ambiguous
}
\end{lstlisting}

调用funcObj(3, 5)视为带有三个参数的调用:funcObj、3和5。候选函数包括成员operator()(视为具有IndirectFunctor const\&、double和double参数类型)和参数类型为FuncType*、double和int的代理函数。代理函数与隐含参数的匹配较差(因为它需要用户定义的转换)，但与最后一个参数的匹配较好;因此，不能使用这俩候选。因此，该调用具有歧义。

代理函数是C++中最晦涩的角落，在实践中很少出现(幸运的是)。

\subsubsubsection{C.3.6\hspace{0.2cm}其他情况的重载}

已经讨论了重载在确定调用表达式中，应该调用哪个函数的上下文中。但在其他一些情况下，必须做出类似的选择。

第一个上下文发生在需要函数地址的时候。考虑下面的例子:

\begin{lstlisting}[style=styleCXX]
int numElems(Matrix const&); // #1
int numElems(Vector const&); // #2
...
int (*funcPtr)(Vector const&) = numElems; // selects #2
\end{lstlisting}

这里，numElems指的是一个重载集，但我们希望知道其中一个函数的地址。然后重载解析尝试将所需的函数类型(本例中是funcPtr的类型)与可用的候选函数匹配。

另一个需要重载解析的上下文是初始化，但这是一个和微妙的主题，超出了附录所能涵盖的范围。一个简单的例子说明了重载解析的特殊情况:

\begin{lstlisting}[style=styleCXX]
#include <string>
class BigNum {
	public:
	BigNum(long n); // #1
	BigNum(double n); // #2
	BigNum(std::string const&); // #3
	...
	operator double(); // #4
	operator long(); // #5
	...
};

void initDemo()
{
	BigNum bn1(100103); // selects #1
	BigNum bn2("7057103224.095764"); // selects #3
	int in = bn1; // selects #5
}
\end{lstlisting}

本例中，需要重载解析来选择适当的构造函数或转换操作符。具体来说，bn1的初始化调用第一个构造函数，bn2的初始化调用第三个构造函数，in()的初始化调用operator long()。大多数情况下，重载规则会产生直观的结果。但这些规则的细节相当复杂，一些(少量)应用程序会依赖于C++语言这一领域中的技术。













